package me.lwn.auth.security.oauth2.server.authorization;

import org.springframework.jdbc.core.*;
import org.springframework.stereotype.Repository;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

import javax.annotation.PostConstruct;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
import java.util.*;
import java.util.function.Function;

@Repository
public class JdbcServerSettingRepository implements ServerSettingRepository {
    private static final String COLUMN_NAMES = "access_token_time_to_live, "
            + "refresh_token_time_to_live, "
            + "reuse_refresh_token, "
            + "state_time_to_live, "
            + "auth_code_time_to_live, "
            + "global_token_setting, "
            + "require_proof_key, "
            + "require_authorization_consent, "
            + "token_endpoint_authentication_signing_algorithm, "
            + "jwk_set_url, "
            + "user_login_fail_lock_times, "
            + "login_fail_locked_duration, "
            + "ip_login_fail_locked, "
            + "require_captcha, "
            + "customize_login_page, "
            + "global_client_setting, "
            + "password_length, "
            + "password_rules, "
            + "password_regular_change, "
            + "password_regular_change_time_interval, "
            + "sms_code_time_to_live ";


    private static final String SERVER_SETTING_TABLE_NAME = "oauth2_server_setting";

    private static final String LOAD_SERVER_SETTING_SQL = "SELECT " + COLUMN_NAMES + " FROM " + SERVER_SETTING_TABLE_NAME;

    private static final String INSERT_SERVER_SETTING_SQL = "INSERT INTO " + SERVER_SETTING_TABLE_NAME
            + "(" + COLUMN_NAMES + ") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";

    private static final String UPDATE_SERVER_SETTING_SQL = "UPDATE " + SERVER_SETTING_TABLE_NAME
            + " SET access_token_time_to_live = ?, refresh_token_time_to_live = ?, reuse_refresh_token = ?, state_time_to_live = ?, "
            + " auth_code_time_to_live = ?, global_token_setting = ?, require_proof_key = ?, require_authorization_consent = ?, "
            + " token_endpoint_authentication_signing_algorithm = ?, jwk_set_url = ?, user_login_fail_lock_times = ?, "
            + " login_fail_locked_duration = ?, ip_login_fail_locked = ?, require_captcha = ?, customize_login_page = ?, "
            + " global_client_setting = ?, password_length = ?, password_rules = ?, password_regular_change = ?, "
            + " password_regular_change_time_interval = ?, sms_code_time_to_live = ?";

    private final JdbcOperations jdbcOperations;
    private RowMapper<ServerSetting> serverSettingRowMapper;
    private Function<ServerSetting, List<SqlParameterValue>> serverSettingParametersMapper;

    public JdbcServerSettingRepository(JdbcOperations jdbcOperations) {
        Assert.notNull(jdbcOperations, "jdbcOperations cannot be null");
        this.jdbcOperations = jdbcOperations;
        this.serverSettingRowMapper = new ServerSettingRowMapper();
        this.serverSettingParametersMapper = new ServerSettingParametersMapper();
    }

    @PostConstruct
    public void init() {
        ServerSetting serverSetting = get();
        if (serverSetting == null) {
            serverSetting = new ServerSetting();
            save(serverSetting);
        }
    }

    public void save(ServerSetting serverSetting) {
        Assert.notNull(serverSetting, "serverSetting cannot be null");
        ServerSetting existingRegisteredClient = findBy();
        if (existingRegisteredClient != null) {
            updateServerSetting(serverSetting);
        } else {
            insertServerSetting(serverSetting);
        }
    }

    private void updateServerSetting(ServerSetting serverSetting) {
        List<SqlParameterValue> parameters = new ArrayList<>(this.serverSettingParametersMapper.apply(serverSetting));
        PreparedStatementSetter pss = new ArgumentPreparedStatementSetter(parameters.toArray());
        this.jdbcOperations.update(UPDATE_SERVER_SETTING_SQL, pss);
    }

    private void insertServerSetting(ServerSetting serverSetting) {
        List<SqlParameterValue> parameters = this.serverSettingParametersMapper.apply(serverSetting);
        PreparedStatementSetter pss = new ArgumentPreparedStatementSetter(parameters.toArray());
        this.jdbcOperations.update(INSERT_SERVER_SETTING_SQL, pss);
    }

    public ServerSetting get() {
        return findBy();
    }

    private ServerSetting findBy() {
        List<ServerSetting> result = this.jdbcOperations.query(
                LOAD_SERVER_SETTING_SQL, this.serverSettingRowMapper);
        Assert.isTrue(result.size() <= 1, "server settings error");
        return !result.isEmpty() ? result.get(0) : null;
    }

    protected final JdbcOperations getJdbcOperations() {
        return this.jdbcOperations;
    }

    protected final RowMapper<ServerSetting> getServerSettingRowMapper() {
        return this.serverSettingRowMapper;
    }

    public final void setServerSettingRowMapper(RowMapper<ServerSetting> serverSettingRowMapper) {
        Assert.notNull(serverSettingRowMapper, "serverSettingRowMapper cannot be null");
        this.serverSettingRowMapper = serverSettingRowMapper;
    }

    protected final Function<ServerSetting, List<SqlParameterValue>> getServerSettingParametersMapper() {
        return this.serverSettingParametersMapper;
    }

    public final void setServerSettingParametersMapper(Function<ServerSetting, List<SqlParameterValue>> serverSettingParametersMapper) {
        Assert.notNull(serverSettingParametersMapper, "serverSettingParametersMapper cannot be null");
        this.serverSettingParametersMapper = serverSettingParametersMapper;
    }

    public static class ServerSettingRowMapper implements RowMapper<ServerSetting> {

        @Override
        public ServerSetting mapRow(ResultSet rs, int rowNum) throws SQLException {
            Set<String> ruleSet = StringUtils.commaDelimitedListToSet(rs.getString("password_rules"));
            Set<Integer> rules = new HashSet<>();
            ruleSet.forEach(rule -> rules.add(Integer.parseInt(rule)));
            ServerSetting serverSetting = new ServerSetting();
            serverSetting.setAccessTokenTimeToLive(rs.getLong("access_token_time_to_live"));
            serverSetting.setRefreshTokenTimeToLive(rs.getLong("refresh_token_time_to_live"));
            serverSetting.setReuseRefreshTokens(rs.getBoolean("reuse_refresh_token"));
            serverSetting.setStateTimeToLive(rs.getLong("state_time_to_live"));
            serverSetting.setAuthCodeTimeToLive(rs.getLong("auth_code_time_to_live"));
            serverSetting.setGlobalTokenSetting(rs.getBoolean("global_token_setting"));

            serverSetting.setRequireProofKey(rs.getBoolean("require_proof_key"));
            serverSetting.setRequireAuthorizationConsent(rs.getBoolean("require_authorization_consent"));
            serverSetting.setTokenEndpointAuthenticationSigningAlgorithm(rs.getString("token_endpoint_authentication_signing_algorithm"));
            serverSetting.setJwkSetUrl(rs.getString("jwk_set_url"));
            serverSetting.setUserLoginFailLockTimes(rs.getInt("user_login_fail_lock_times"));
            serverSetting.setLoginFailLockedDuration(rs.getInt("login_fail_locked_duration"));
            serverSetting.setIpLoginFailLocked(rs.getBoolean("ip_login_fail_locked"));
            serverSetting.setRequireCaptcha(rs.getBoolean("require_captcha"));
            serverSetting.setCustomizeLoginPage(rs.getBoolean("customize_login_page"));
            serverSetting.setGlobalClientSetting(rs.getBoolean("global_client_setting"));

            serverSetting.setPasswordLength(rs.getInt("password_length"));
            serverSetting.setPasswordRules(rules);
            serverSetting.setPasswordRegularChange(rs.getBoolean("password_regular_change"));
            serverSetting.setPasswordRegularChangeTimeInterval(rs.getInt("password_regular_change_time_interval"));
            serverSetting.setSmsCodeTimeToLive(rs.getLong("sms_code_time_to_live"));

            return serverSetting;
        }
    }

    public static class ServerSettingParametersMapper implements Function<ServerSetting, List<SqlParameterValue>> {

        @Override
        public List<SqlParameterValue> apply(ServerSetting serverSetting) {
            return Arrays.asList(
                    new SqlParameterValue(Types.BIGINT, serverSetting.getAccessTokenTimeToLive()),
                    new SqlParameterValue(Types.BIGINT, serverSetting.getRefreshTokenTimeToLive()),
                    new SqlParameterValue(Types.BOOLEAN, serverSetting.isReuseRefreshTokens()),
                    new SqlParameterValue(Types.BIGINT, serverSetting.getStateTimeToLive()),
                    new SqlParameterValue(Types.BIGINT, serverSetting.getAuthCodeTimeToLive()),
                    new SqlParameterValue(Types.BOOLEAN, serverSetting.isGlobalTokenSetting()),

                    new SqlParameterValue(Types.BOOLEAN, serverSetting.isRequireProofKey()),
                    new SqlParameterValue(Types.BOOLEAN, serverSetting.isRequireAuthorizationConsent()),
                    new SqlParameterValue(Types.VARCHAR, serverSetting.getTokenEndpointAuthenticationSigningAlgorithm()),
                    new SqlParameterValue(Types.VARCHAR, serverSetting.getJwkSetUrl()),
                    new SqlParameterValue(Types.INTEGER, serverSetting.getUserLoginFailLockTimes()),
                    new SqlParameterValue(Types.INTEGER, serverSetting.getLoginFailLockedDuration()),
                    new SqlParameterValue(Types.BOOLEAN, serverSetting.isIpLoginFailLocked()),
                    new SqlParameterValue(Types.BOOLEAN, serverSetting.isRequireCaptcha()),
                    new SqlParameterValue(Types.BOOLEAN, serverSetting.isCustomizeLoginPage()),
                    new SqlParameterValue(Types.BOOLEAN, serverSetting.isGlobalClientSetting()),

                    new SqlParameterValue(Types.INTEGER, serverSetting.getPasswordLength()),
                    new SqlParameterValue(Types.VARCHAR, StringUtils.collectionToCommaDelimitedString(serverSetting.getPasswordRules())),
                    new SqlParameterValue(Types.BOOLEAN, serverSetting.isPasswordRegularChange()),
                    new SqlParameterValue(Types.INTEGER, serverSetting.getPasswordRegularChangeTimeInterval()),
                    new SqlParameterValue(Types.BIGINT, serverSetting.getSmsCodeTimeToLive())
            );
        }
    }
}
